package com.google.code.lightsomy.config;

import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import com.google.code.lightsomy.annotations.Action;
import com.google.code.lightsomy.annotations.Controller;
import com.google.code.lightsomy.http.LightsomyConstants;
import com.google.code.lightsomy.route.LightsomyMapper;
import com.google.code.lightsomy.route.Mapper;
import com.google.code.lightsomy.route.Route;
import com.google.code.lightsomy.section.Section;
import com.google.code.lightsomy.util.AnnotatedClassSearch;
import com.google.code.lightsomy.util.OrderedProperties;

/**
 * Main Lightsomy config. 
 * Gathers configuration information from properties file.
 * 
 * @author Viktoras Agejevas
 *
 */
public class LightsomyConfig {

	/**
	 * Holds mappers for each section
	 */
	private Map<Section, Mapper> mappers;
	
	/**
	 * List of all configured sections
	 */
	private List<Section> sections;
	
	/**
	 * Routes for each section
	 */
	private Map<Section, List<Route>> routes;
	
	/**
	 * Properties file instance
	 */
	private OrderedProperties properties;
	
	/**
	 * Holds default Lightsomy routes
	 */
	private List<Route> defaultRoutes;
	
	private static Log log = LogFactory.getLog(LightsomyConfig.class);
	
	/**
	 * Create Lightsomy configuration from properties file.
	 * 
	 * @param resource path to a properties file
	 */
	public LightsomyConfig(String resource) {
		try {
			readProperties(resource);
		} catch (IOException e) {
			log.warn("Can't read Lightsomy configuration", e);
			properties = new OrderedProperties();
		}
		parseSections();
		parseRoutes();
		configureMappers();
	}
	
	/**
	 * Loads/Reads properties file.
	 * 
	 * @param resource path to a properties file
	 * @throws IOException
	 */
	private void readProperties(String resource) throws IOException {
		InputStream inputStream = null;
		properties = new OrderedProperties();

		try {
			ClassLoader cl = Thread.currentThread().getContextClassLoader();
			inputStream = cl.getResourceAsStream(resource);
			if (inputStream != null) {
				properties.load(inputStream);
			} else {
				throw new IOException("Can't get resource: " + resource);
			}
		} finally {
			if (inputStream != null) {
				inputStream.close();
			}
		}
	}
	
	/**
	 * Parses sections from loaded properties file.
	 */
	private void parseSections() {
		sections = new ArrayList<Section>();
		
		if (properties.isEmpty()) {
			sections.add(new Section("default", "*"));
			return;
		}
		
		for (Entry<Object, Object> section : properties.entrySet()) {
			String name = (String) section.getKey();
			if (name.startsWith("section")) {
				String packageName = (String) section.getValue();
				sections.add(new Section(name.replace("section.", "").replaceAll("\\.", "/"), packageName));
			}
		}
	}
	
	/**
	 * Parses routes for all sections.
	 */
	private void parseRoutes() {
		routes = new HashMap<Section, List<Route>>();
		for (Section s : sections) {
			List<Route> routes = new ArrayList<Route>();
			for (Entry<Object, Object> entry : properties.entrySet()) {
				String name = (String) entry.getKey();
				if (name.startsWith(s.getName().replace("/", "."))) {
					String info = (String) entry.getValue();
					String pattern = parsePattern(info);
					String controller = parseController(info);
					String action = parseAction(info);
					
					if (pattern != null && controller != null && action != null) {
						routes.add(new Route(name, pattern, controller, action));
					}
				}
			}
			this.routes.put(s, routes);
		}
	}
	
	/**
	 * Configures a mapper for each section.
	 */
	private void configureMappers() {
		mappers = new HashMap<Section, Mapper>();
		for (Section section : sections) {
			Mapper m = new LightsomyMapper();
			m.addAllRoutes(getAnnotatedRoutes(section));
			m.addAllRoutes(getUserRoutes(section));
			m.addAllRoutes(getDefaultRoutes());
			mappers.put(section, m);
		}
	}

	private String parsePattern(String info) {
		return parseRouteParam(info, "pattern");
	}
	
	private String parseController(String info) {
		return parseRouteParam(info, "controller");
	}
	
	private String parseAction(String info) {
		return parseRouteParam(info, "action");
	}
	
	private String parseRouteParam(String info, String part) {
		Pattern p = Pattern.compile(part + ":(\\s+)?([^\\s,$]+)");
		Matcher m = p.matcher(info);

		if (m.find()) {
			return m.group(2);
		}
		
		return null;
	}
	
	/**
	 * List of default lightsomy routes.
	 * 
	 * @return list of default routes.
	 */
	protected List<Route> getDefaultRoutes() {
		if (defaultRoutes != null) {
			return defaultRoutes;
		}
		
		defaultRoutes = new ArrayList<Route>();
		String ctrl = LightsomyConstants.CONTROLLER.getValue();
		String act = LightsomyConstants.ACTION.getValue();
		String id = LightsomyConstants.ID.getValue();
		defaultRoutes.add(new Route("^/(<" + ctrl + ">\\w+?)$"));
		defaultRoutes.add(new Route("^/(<" + ctrl + ">\\w+?)/(<" + id + ">\\d+?)$"));
		defaultRoutes.add(new Route("^/(<" + ctrl + ">\\w+?)/(<" + act + ">\\w+?)$"));
		defaultRoutes.add(new Route("^/(<" + ctrl + ">\\w+?)/(<" + act + ">\\w+?)/(<" + id + ">\\d+?)$"));
		return defaultRoutes;
	}
	
	/**
	 * List of annotated routes for supplied section.
	 * 
	 * @param section
	 * @return list of annotated routes
	 */
	private List<Route> getAnnotatedRoutes(Section section) {
		Map<String, String> controllers = 
			AnnotatedClassSearch.search(section.getPackageName(), Controller.class);
		List<Route> routes = new ArrayList<Route>();
		
		for (Entry<String, String> info : controllers.entrySet()) {
			try {
				Object controller = Class.forName(info.getValue()).newInstance();
				Class<?> clazz = controller.getClass();
				for (Method method : clazz.getMethods()) {
					if (method.isAnnotationPresent(Action.class)) {
						String r = method.getAnnotation(Action.class).route();
						if (!"".equals(r)) {
							String name = clazz.getSimpleName() + "_" + method.getName();
							routes.add(new Route(name, r, clazz.getSimpleName(), method.getName()));
						}
					}
				}
			} catch (InstantiationException e) {
				log.warn("Can't instantiate" + info.getValue(), e);
			} catch (Exception e) {
				log.error("Failed to add annotated route", e);
			}
		}
		
		return routes;
	}
	
	/**
	 * List of user routes (from properties file) for section.
	 * 
	 * @param section
	 * @return list of user routes
	 */
	private List<Route> getUserRoutes(Section section) {
		return routes.get(section);
	}

	/**
	 * Returns configured mappers for each section.
	 * 
	 * @return map of mappers
	 */
	public Map<Section, Mapper> getMappers() {
		return Collections.unmodifiableMap(mappers);
	}

	/**
	 * Returns list of sections defined for this application.
	 * 
	 * @return list of sections
	 */
	public List<Section> getSections() {
		return Collections.unmodifiableList(sections);
	}
}
